﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.Excel.OpenXmlSdk;
using ScrumTable.Common.Excel.OpenXmlSdk.Extensions;
using ScrumTable.Common.FileSystem;
using ScrumTable.Common.Logging;
using ScrumTable.DL.Data.Excel.Services.Excel;
using ScrumTable.DL.Data.Excel.Properties;
using ScrumTable.DL.Data.Generic.Cache;
using ScrumTable.DL.Data.Generic.Services;

#endregion

namespace ScrumTable.DL.Data.Excel.Services
{
    /// <summary>
    /// This class is used to synchronize data from the data layer to the
    /// XML data storage.
    /// </summary>
    internal class DataSaveProcess
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private const string AttachmentFileNamePattern = "{0}_{1}";
        private const string AssetsFolder = "Assets";

        private readonly CacheDataChanges _changes;
        private readonly AsyncDataContextInfo _dataContext;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new DataSaveProcess instance, no public constructor
        /// needed.
        /// </summary>
        /// <param name="dataContext">Specifies the data context which contains the underlying data driver root object.</param>
        /// <param name="changes">Specifies the change set to synchronize.</param>
        internal DataSaveProcess(AsyncDataContextInfo dataContext, CacheDataChanges changes)
        {
            PreCondition.AssertNotNull(dataContext, "dataContext");
            PreCondition.AssertNotNull(changes, "changes");

            _dataContext = dataContext;
            _changes = changes;
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Synchronizes the changes with the team foundation server and
        /// sets the accepted flag if the change has been successfully
        /// stored back to the back end system.
        /// </summary>
        internal void SyncChanges()
        {
            Exception serializationError = null;

            if (_changes.ChangedElementCount > 0)
            {
                try
                {
                    StoreExcelData();
                }
                catch (Exception e)
                {
                    serializationError = e;
                    Logger<DataSaveProcess>.Error(Resources.ExcDataSvProcFailed, e);
                }
            }

            if (serializationError == null)
            {
                _changes.RemoveDirtyFlags();
                _changes.MarkChangesAs(CacheSavePairState.Saved);
            }
            else
            {
                _changes.MarkChangesAsError(serializationError);
            }
        }

        private void StoreExcelData()
        {
            WorkbookMapper excelWorkbookMapper = new WorkbookMapper(
                new OpenXmlWorkbookSerializer(_dataContext.ConnectionMoniker),
                _dataContext.Schema);

            StoreIterations(excelWorkbookMapper);
            StoreWorkItems(excelWorkbookMapper);

            excelWorkbookMapper.Save();
        }

        private void StoreIterations(WorkbookMapper excelWorkbookMapper)
        {
            if (!excelWorkbookMapper.IterationsSheet.IsContained) { return; }

            foreach (var iterationPair in _changes.ChangedIterations)
            {
                int iterationRowId = iterationPair.Value.Element.Id;
                string iterationRowName = iterationPair.Value.Element.Name;
                DataRow iterationRow = excelWorkbookMapper.IterationsSheet.GetOrCreateDataRow(ref iterationRowId);
                string refId = RefIdConverter.ToRefId(iterationRowId, iterationRowName);

                iterationRow.SetColumnValue(excelWorkbookMapper.GetRefIdFieldName(_dataContext.Schema.IterationSchema), refId);
                iterationRow.SetColumnValue(_dataContext.Schema.IterationSchema.EndTime.SchemaMappedName, iterationPair.Value.Element.EndDate);
                iterationRow.SetColumnValue(_dataContext.Schema.IterationSchema.StartTime.SchemaMappedName, iterationPair.Value.Element.StartDate);
            }
        }

        private void StoreWorkItems(WorkbookMapper excelWorkbookMapper)
        {
            IDictionary<string, WorkItemTypeSheetMeta> scheetMappings = _changes.Project.WorkItemTypes.ConvertIf(
                wiType => excelWorkbookMapper.GetWorkItemSheetMapping(wiType.Name) != null,
                wiType => new KeyValuePair<string, WorkItemTypeSheetMeta>(
                    wiType.Name,
                    new WorkItemTypeSheetMeta { Type = wiType, Worksheet = excelWorkbookMapper.GetWorkItemSheetMapping(wiType.Name)}));

            IDictionary<string, WorkItemSaveContext> localWorkItemStore = CreateOrRetrieveWorkItems(scheetMappings);

            FillIdsIntoCacheTree(localWorkItemStore);
            StoreWorkItemValues(excelWorkbookMapper, localWorkItemStore);
            EstablishLinksToReferencedElements(excelWorkbookMapper, scheetMappings, localWorkItemStore);
        }

        private void FillIdsIntoCacheTree(IDictionary<string, WorkItemSaveContext> localWorkItemStore)
        {
            // update all references (link id's) in the work item store (loop through all ChangedWorkItems is important)
            foreach (var cacheSavePair in _changes.ChangedWorkItems)
            {
                if (localWorkItemStore.ContainsKey(cacheSavePair.Value.Element.UniqueId))
                {
                    cacheSavePair.Value.Element.AssignId(localWorkItemStore[cacheSavePair.Value.Element.UniqueId].WorkItem.Id);
                }

                FillIdsIntoCacheLinkTree(localWorkItemStore, cacheSavePair.Value.Element.ChildLinks);
                FillIdsIntoCacheLinkTree(localWorkItemStore, cacheSavePair.Value.Element.ParentLinks);
            }
        }

        private void FillIdsIntoCacheLinkTree(IDictionary<string, WorkItemSaveContext> localWorkItemStore, CacheElementList<CacheWorkItemLink> relationLinks)
        {
            foreach (CacheWorkItemLink link in relationLinks)
            {
                if (localWorkItemStore.ContainsKey(link.RelatedUnqiueId))
                {
                    link.RelatedId = localWorkItemStore[link.RelatedUnqiueId].WorkItem.Id;
                }
            }
        }

        private IDictionary<string, WorkItemSaveContext> CreateOrRetrieveWorkItems(IDictionary<string, WorkItemTypeSheetMeta> scheetMappings)
        {
            IDictionary<string, WorkItemSaveContext> localWorkItemStore = new Dictionary<string, WorkItemSaveContext>();

            foreach (var workItemPair in _changes.ChangedWorkItems)
            {
                #region Make sure excel table exists.

                string excelTableName = workItemPair.Value.Element.Type.Name;
                
                if (!scheetMappings.ContainsKey(excelTableName)) { continue; }

                #endregion
                
                #region Make sure work item excel row exists or create a new row.

                CacheWorkItem workItem = workItemPair.Value.Element;
                int workItemRowId = workItem.Id;
                DataRow workItemRow = scheetMappings[excelTableName].Worksheet.GetOrCreateDataRow(ref workItemRowId);

                workItem.AssignId(workItemRowId);

                localWorkItemStore[workItem.UniqueId] = new WorkItemSaveContext(
                    scheetMappings[excelTableName].Worksheet,
                    workItem,
                    workItemRow,
                    RefIdConverter.ToRefId(workItemRowId, workItem.Name));

                #endregion
            }
            return localWorkItemStore;
        }

        private void StoreWorkItemValues(WorkbookMapper excelWorkbookMapper, IDictionary<string, WorkItemSaveContext> localWorkItemStore)
        {
            // store ALL values into the data table; inclusive all names.
            // these names are needed to establish the parent references in the Parent ID column
            foreach (var workItemSavePair in localWorkItemStore)
            {
                StoreWorkItem(excelWorkbookMapper, workItemSavePair.Value);
            }
        }

        private void StoreWorkItem(WorkbookMapper excelWorkbookMapper, WorkItemSaveContext context)
        {
            context.WorkItemRow.SetColumnValue(/* Name ID */ excelWorkbookMapper.GetRefIdFieldName(context.ExcelSheetMapping.Schema), context.WorkItemRefId);
            context.WorkItemRow.SetColumnValue(/* Name */ context.WorkItem.Type.NameField.Schema.SchemaMappedName, context.WorkItem.Name);
            context.WorkItemRow.SetColumnValue(/* Description */ context.WorkItem.Type.DescriptionField.Schema.SchemaMappedName, context.WorkItem.Description);
            context.WorkItemRow.SetColumnValue(
                /* State */ context.WorkItem.Type.StateField.Schema.SchemaMappedName,
                Equals(context.WorkItem.Type.StateField.DefaultValue, context.WorkItem.State) ? SheetValueDefinition.ToDefaultValue(context.WorkItem.State) : context.WorkItem.State);
            context.WorkItemRow.SetColumnValue(
                /* Member ID */ context.WorkItem.Type.MemberIdField.Schema.SchemaMappedName,
                (context.WorkItem.AssignedToMember != null) ? context.WorkItem.AssignedToMember.ReferenceName : null);
            context.WorkItemRow.SetColumnValue(
                /* Iteration ID */ excelWorkbookMapper.GetIterationIdFieldName(context.WorkItem.Type.Schema),
                (context.WorkItem.Iteration != null) ? RefIdConverter.ToRefId(context.WorkItem.Iteration.Id, context.WorkItem.Iteration.Name) : null);

            FillDynamicWorkItemValues(context);
        }

        private void FillDynamicWorkItemValues(WorkItemSaveContext source)
        {
            // the names of the ICacheTrackedDictionaryBucket correspond to the
            // BackEndId (also the SchemaMappedName) of the CacheFieldDefinitions
            //  -> see DataProvider.GetAssignedValues()
            foreach (ICacheTrackedDictionaryBucket changedValueBucket in source.WorkItem.ChangedValues)
            {
                SaveValue(source, changedValueBucket);
            }
        }

        private void SaveValue(WorkItemSaveContext source, ICacheTrackedDictionaryBucket changedValueBucket)
        {
            CacheFieldDefinition fieldDef = source.WorkItem.Type.TryGetFieldByBackEndId(changedValueBucket.Name);

            if (fieldDef != null)
            {
                source.WorkItemRow.SetColumnValue(
                    fieldDef.Schema.SchemaMappedName,
                    changedValueBucket.Value);
            }
        }

        private void EstablishLinksToReferencedElements(
            WorkbookMapper excelWorkbookMapper,
            IDictionary<string, WorkItemTypeSheetMeta> scheetMappings,
            IDictionary<string, WorkItemSaveContext> localWorkItemStore)
        {
            // store ALL links into the data table (references to names of other work items)
            foreach (var workItemSavePair in localWorkItemStore.Values)
            {
                EstablishParentWorkItemLink(excelWorkbookMapper, scheetMappings, workItemSavePair);
                RepairChildWorkItemLinks(excelWorkbookMapper, scheetMappings, workItemSavePair);
                StoreWorkItemAttachments(excelWorkbookMapper, workItemSavePair);
            }
        }

        private void StoreWorkItemAttachments(
            WorkbookMapper excelWorkbookMapper,
            WorkItemSaveContext context)
        {
            if (!excelWorkbookMapper.AttachmentsSheet.IsContained) { return; }

            foreach (CacheAttachment atta in context.WorkItem.Attachments)
            {
                int attaRowId = atta.Id;
                DataRow attaRow = excelWorkbookMapper.AttachmentsSheet.GetOrCreateDataRow(ref attaRowId);
                atta.AssignId(attaRowId);

                attaRow.SetColumnValue(
                    /* Parent ID */ excelWorkbookMapper.GetParentIdFieldName(excelWorkbookMapper.AttachmentsSheet.Schema),
                    context.WorkItemRefId);

                if (atta.IsDirty)
                {
                    Uri localAttachmentUri = GetAttachmentFilePath(atta);

                    attaRow.SetColumnValue(
                        /* Name */ _dataContext.Schema.AttachmentSchema.Name.SchemaMappedName,
                        atta.Name);

                    attaRow.SetColumnValue(
                        /* Description */ _dataContext.Schema.AttachmentSchema.Description.SchemaMappedName,
                        atta.Description);

                    attaRow.SetColumnValue(
                        /* Uri */ _dataContext.Schema.AttachmentSchema.Uri.SchemaMappedName,
                        localAttachmentUri.ToLocalPath(),
                        localAttachmentUri);
                }
            }
        }

        private Uri GetAttachmentFilePath(CacheAttachment atta)
        {
            Uri newFilePath = null;

            if (atta.Uri != null)
            {
                FileInfo currentFile = new FileInfo(atta.Uri.ToLocalPath());
                
                if (currentFile.Exists)
                {
                    string excelFileFolder = Path.GetDirectoryName(_dataContext.ConnectionMoniker);
                    DirectoryInfo assetsFolder = new DirectoryInfo(Path.Combine(excelFileFolder, AssetsFolder));
                    assetsFolder.EnsureExists();

                    string physicalFilePath = Path.Combine(
                        assetsFolder.FullName,
                        PathUtil.ToValidFileName(string.Format(AttachmentFileNamePattern, atta.Id, atta.Name)));

                    currentFile.CopyTo(physicalFilePath, true);

                    newFilePath = new Uri(
                        physicalFilePath.Substring(
                            excelFileFolder.Length + 1,
                            physicalFilePath.Length - excelFileFolder.Length - 1),
                        UriKind.RelativeOrAbsolute);
                }
            }
            return newFilePath;
        }

        private void RepairChildWorkItemLinks(
            WorkbookMapper excelWorkbookMapper,
            IDictionary<string, WorkItemTypeSheetMeta> scheetMappings,
            WorkItemSaveContext context)
        {
            foreach (var workItemChildLink in context.WorkItem.ChildLinks)
            {
                WorkItemSheetMeta childRowMeta = GetRelatedWorkItemRow(scheetMappings, workItemChildLink.RelatedId);

                if (childRowMeta != null)
                {
                    childRowMeta.Data.SetColumnValue(
                        /* Parent ID */ excelWorkbookMapper.GetParentIdFieldName(childRowMeta.Type.Schema),
                        context.WorkItemRefId);
                }
            }
        }

        private void EstablishParentWorkItemLink(
            WorkbookMapper excelWorkbookMapper,
            IDictionary<string, WorkItemTypeSheetMeta> scheetMappings,
            WorkItemSaveContext context)
        {
            string parentIdFieldName = excelWorkbookMapper.GetParentIdFieldName(context.WorkItem.Type.Schema);

            if (string.IsNullOrEmpty(parentIdFieldName)) { return; }

            if (context.WorkItem.ParentLink == null)
            {
                context.WorkItemRow.SetColumnValue(parentIdFieldName, null);
            }
            else
            {
                context.WorkItemRow.SetColumnValue(
                    parentIdFieldName,
                    GetRelatedWorkItemRef(scheetMappings, context.WorkItem.ParentLink.RelatedId));
            }
        }

        private string GetRelatedWorkItemRef(IDictionary<string, WorkItemTypeSheetMeta> scheetMappings, int workItemId)
        {
            WorkItemSheetMeta parentRowMeta = GetRelatedWorkItemRow(scheetMappings, workItemId);

            if (parentRowMeta != null)
            {
                return RefIdConverter.ToRefId(
                    workItemId,
                    parentRowMeta.Data.GetColumnValue(parentRowMeta.Type.NameField.Schema.SchemaMappedName, string.Empty));
            }
            else
            {
                return ObjectUtil.ConvertToString(workItemId);
            }
        }

        private WorkItemSheetMeta GetRelatedWorkItemRow(IDictionary<string, WorkItemTypeSheetMeta> scheetMappings, int rowId)
        {
            foreach (WorkItemTypeSheetMeta sheetMetaMapping in scheetMappings.Values)
            {
                DataRow parentRow;

                if (sheetMetaMapping.Worksheet.TryGetById(rowId, out parentRow))
                {
                    return new WorkItemSheetMeta { Data = parentRow, Type = sheetMetaMapping.Type };
                }
            }
            return null;
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        #endregion



        //--------------------------------------------------------------------
        // Nested Classes
        //--------------------------------------------------------------------

        private class WorkItemSaveContext
        {
            #region Declarations
            //--------------------------------------------------------------------
            // Declarations
            //--------------------------------------------------------------------

            #endregion

            #region Properties
            //--------------------------------------------------------------------
            // Properties
            //--------------------------------------------------------------------

            internal CacheWorkItem WorkItem { get; private set; }
            internal DataRow WorkItemRow { get; private set; }
            internal string WorkItemRefId { get; private set; }
            internal SheetSchemaMapping ExcelSheetMapping { get; private set; }

            #endregion

            #region Constructors / Destructor
            //--------------------------------------------------------------------
            // Constructors / Destructor
            //--------------------------------------------------------------------

            internal WorkItemSaveContext(
                SheetSchemaMapping excelSheetMapping,
                CacheWorkItem workItem,
                DataRow workItemRow,
                string workItemRefId)
            {
                PreCondition.AssertNotNull(excelSheetMapping, "excelSheetMapping");
                PreCondition.AssertNotNull(workItem, "workItem");
                PreCondition.AssertNotNull(workItemRow, "workItemRow");
                PreCondition.AssertNotNullOrEmpty(workItemRefId, "workItemRefId");

                ExcelSheetMapping = excelSheetMapping;
                WorkItem = workItem;
                WorkItemRow = workItemRow;
                WorkItemRefId = workItemRefId;
            }

            #endregion
        }


        private class WorkItemTypeSheetMeta
        {
            #region Properties
            //--------------------------------------------------------------------
            // Properties
            //--------------------------------------------------------------------

            internal CacheWorkItemType Type { get; set; }
            internal SheetSchemaMapping Worksheet { get; set; }

            #endregion
        }


        private class WorkItemSheetMeta
        {
            #region Properties
            //--------------------------------------------------------------------
            // Properties
            //--------------------------------------------------------------------

            internal CacheWorkItemType Type { get; set; }
            internal DataRow Data { get; set; }

            #endregion
        }
    }
}

